package com.bugsnag.ohos;

import ohos.agp.window.service.DisplayAttributes;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;
import ohos.batterymanager.BatteryInfo;
import ohos.bundle.AbilityInfo;
import ohos.location.Locator;

import com.bugsnag.ohos.util.OhosLog;

import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;

/**
 * DeviceDataCollector
 *
 * @since 2021-07-05
 */
public class DeviceDataCollector {
    BackgroundTaskService bgTaskService;
    private Connectivity connectivity;
    private Context appContext;
    private String deviceId;
    private DeviceBuildInfo buildInfo;
    private File dataDirectory;
    private RootDetector rootDetector;
    private Logger logger;
    private DisplayAttributes displayMetrics;
    private boolean emulator;
    private float screenDensity;
    private int dpi;
    private String screenResolution;
    private String locale;
    private String[] cpuAbi;
    private Map<String, Object> runtimeVersions;
    private Future<Boolean> rootedFuture;
    private AtomicReference<AbilityInfo.DisplayOrientation> orientation;

    /**
     * constructor
     *
     * @param connectivity
     * @param appContext
     * @param deviceId
     * @param buildInfo
     * @param dataDirectory
     * @param rootDetector
     * @param bgTaskService
     * @param logger
     */
    public DeviceDataCollector(Connectivity connectivity, Context appContext, String deviceId,
                               DeviceBuildInfo buildInfo, File dataDirectory, RootDetector rootDetector,
                               BackgroundTaskService bgTaskService, Logger logger) {
        this.connectivity = connectivity;
        this.appContext = appContext;
        this.deviceId = deviceId;
        this.buildInfo = buildInfo;
        this.dataDirectory = dataDirectory;
        this.rootDetector = rootDetector;
        this.bgTaskService = bgTaskService;
        this.logger = logger;
        this.displayMetrics = DisplayManager.getInstance().getDefaultDisplay(appContext)
            .get().getAttributes();
        this.emulator = isEmulator();
        this.screenDensity = getScreenDensity();
        this.dpi = getScreenDensityDpi();
        this.screenResolution = getScreenResolution();
        this.locale = Locale.getDefault().toString();
        this.cpuAbi = getCpuAbi();
        Map<String, Object> map = new HashMap<>();
        if (buildInfo.apiLevel != 0) {
            map.put("ohosApiLevel", buildInfo.apiLevel);
        }
        if (buildInfo.osBuild != null) {
            map.put("osBuild", buildInfo.osBuild);
        }
        this.runtimeVersions = map;
        try {
            this.rootedFuture = bgTaskService.submitTask(TaskType.IO, () -> rootDetector.isRooted());
        } catch (RejectedExecutionException exc) {
            logger.w("Failed to perform root detection checks", exc);
            this.rootedFuture = null;
        }
        this.orientation = new AtomicReference<>();
    }

    /**
     * generate device
     *
     * @return Device
     */
    public Device generateDevice() {
        return new Device(buildInfo,
            cpuAbi,
            checkIsRooted(),
            deviceId,
            locale,
            calculateTotalMemory(),
            runtimeVersions);
    }

    /**
     * generate Device With State
     *
     * @param time
     * @return DeviceWithState
     */
    public DeviceWithState generateDeviceWithState(long time) {
        return new DeviceWithState(buildInfo,
            checkIsRooted(),
            deviceId,
            locale,
            calculateTotalMemory(),
            runtimeVersions,
            calculateFreeDisk(),
            calculateFreeMemory(),
            getOrientationAsString(),
            new Date(time));
    }

    /**
     * getDeviceMetadata
     *
     * @return meta data
     */
    public Map<String, Object> getDeviceMetadata() {
        Map map = new HashMap<String, Object>();
        populateBatteryInfo(map);
        map.put("locationStatus", getLocationStatus());
        map.put("networkAccess", getNetworkAccess());
        map.put("brand", buildInfo.brand);
        map.put("screenDensity", screenDensity);
        map.put("dpi", dpi);
        map.put("emulator", emulator);
        map.put("screenResolution", screenResolution);
        return map;
    }

    private boolean checkIsRooted() {
        boolean isRooted;
        try {
            isRooted = rootedFuture != null && rootedFuture.get();
        } catch (ExecutionException e) {
            isRooted = false;
        } catch (InterruptedException e) {
            isRooted = false;
        }
        return isRooted;
    }

    /**
     * Guesses whether the current device is an emulator or not, erring on the side of caution
     *
     * @return true if the current device is an emulator
     */
    private boolean isEmulator() {
        String fingerprint = buildInfo.fingerprint;
        return fingerprint != null && (
            fingerprint.startsWith("unknown")
                || fingerprint.contains("generic")
                || fingerprint.contains("vbox"));
    }

    /**
     * The screen density of the current ohos device in dpi, eg. 320
     */
    private int getScreenDensityDpi() {
        DisplayAttributes attributes = DisplayManager.getInstance().getDefaultDisplay(appContext).
            get().getRealAttributes();
        return attributes.densityDpi;
    }

    /**
     * Populate the current Battery Info into the specified MutableMap
     */
    private void populateBatteryInfo(Map<String, Object> into) {
        BatteryInfo batteryInfo = new BatteryInfo();
        int capacity = batteryInfo.getCapacity();
        float c = capacity / 100;
        OhosLog.d("DeviceDataCollector", "capacity = %d:", capacity);
        BatteryInfo.BatteryChargeState status = batteryInfo.getChargingStatus();
        boolean charging = status == BatteryInfo.BatteryChargeState.ENABLE
            || status == BatteryInfo.BatteryChargeState.FULL;
        into.put("charging", charging);
        into.put("batteryLevel", c);
        OhosLog.d("DeviceDataCollector", "charging = %s:", charging);
    }

    /**
     * Get the current status of location services
     */
    private String getLocationStatus() {
        Locator locator = new Locator(appContext);
        if (locator.isLocationSwitchOn()) {
            return "allowed";
        } else {
            return "disallowed";
        }
    }

    /**
     * Get the current status of network access, eg "cellular"
     *
     * @return network access state
     */
    private String getNetworkAccess() {
        return connectivity.retrieveNetworkAccessState();
    }

    /**
     * The screen density scaling factor of the current ohos device
     *
     * @return Screen Density
     */
    private float getScreenDensity() {
        DisplayAttributes attributes = DisplayManager.getInstance().getDefaultDisplay(appContext).
            get().getRealAttributes();
        return attributes.densityPixels;
    }

    /**
     * The screen resolution of the current ohos device in px, eg. 1920x1080
     *
     * Screen Resolution
     */
    private String getScreenResolution() {
        DisplayAttributes attributes = DisplayManager.getInstance().getDefaultDisplay(appContext).
            get().getRealAttributes();
        int width = attributes.width;
        int height = attributes.height;
        return width + "x" + height;
    }

    /**
     * Gets information about the CPU / API
     */
    String[] getCpuAbi() {
        if (buildInfo.cpuAbis == null) {
            return new String[]{};
        }
        return buildInfo.cpuAbis;
    }

    /**
     * Get the usable disk space on internal storage's data directory
     */
    long calculateFreeDisk() {
        // for this specific case we want the currently usable space, not
        // StorageManager#allocatableBytes() as the UsableSpace lint inspection suggests
        return dataDirectory.getUsableSpace();
    }

    /**
     * Get the amount of memory remaining that the VM can allocate
     */
    private long calculateFreeMemory() {
        Runtime runtime = Runtime.getRuntime();
        long maxMemory = runtime.maxMemory();
        if (maxMemory != Long.MAX_VALUE) {
            return maxMemory - runtime.totalMemory() + runtime.freeMemory();
        } else {
            return runtime.freeMemory();
        }
    }

    /**
     * Get the total memory available on the current ohos device, in bytes
     */
    private long calculateTotalMemory() {
        Runtime runtime = Runtime.getRuntime();
        long maxMemory = runtime.maxMemory();
        if (maxMemory != Long.MAX_VALUE) {
            return maxMemory;
        } else {
            return runtime.totalMemory();
        }
    }

    /**
     * Get the device orientation, eg. "landscape"
     */
    String getOrientationAsString() {
        AbilityInfo.DisplayOrientation displayOrientation = orientation.get();
        if (displayOrientation == AbilityInfo.DisplayOrientation.LANDSCAPE) {
            return "landscape";
        } else if (displayOrientation == AbilityInfo.DisplayOrientation.PORTRAIT) {
            return "portrait";
        } else {
            return null;
        }
    }

    /**
     * Called whenever the orientation is updated so that the device information is accurate.
     * Currently this is only invoked by [ClientComponentCallbacks]. Returns true if the
     * orientation has changed, otherwise false.
     */
    boolean updateOrientation(AbilityInfo.DisplayOrientation newOrientation) {
        return orientation.getAndSet(newOrientation) != newOrientation;
    }

    void addRuntimeVersionInfo(String key, String value) {
        runtimeVersions.put(key, value);
    }
}
